package org.codeanalyzer.xml;

import java.io.File;
import java.io.FileWriter;
import java.io.Writer;
import java.util.Arrays;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.codeanalyzer.model.ApiClass;
import org.codeanalyzer.model.ApiLibrary;
import org.codeanalyzer.model.ApiMethod;
import org.codeanalyzer.model.ApiPackage;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/**
 * Allows to save apiLibrary to an *xml file and then retrieve it from it.
 * 
 * @author edermengi
 */
public class XmlService
{

    /**
     * Saves apiLibrary and all its packages, classes and methods to specified
     * *.xml file. The *.xml file ISO-8859-1 encoding and human readable format.
     * 
     * @param apiLibrary
     *            the api library to save
     * @param file
     *            the xml file
     */
    public void save(ApiLibrary apiLibrary, File file)
    {
        try
        {
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            Document doc = factory.newDocumentBuilder().newDocument();
            addElementLibrary(doc, apiLibrary);

            FileWriter fileWriter = new FileWriter(file);
            try
            {
                writeDocument(doc, fileWriter);
            }
            finally
            {
                fileWriter.close();
            }
        }
        catch(Exception e)
        {
            throw new RuntimeException("Unable to save API library data to the file " + file.getPath(), e);
        }
    }

    /**
     * Loads apiLibrary and all its packages, classes and methods from the
     * specified file.
     * 
     * @param file
     *            the xml file
     * @return ApiLibrary instance, null if there is no correspondent xml
     *         element for apiLibrary
     */
    public ApiLibrary load(File file)
    {
        try
        {
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            DocumentBuilder builder = factory.newDocumentBuilder();

            Document doc = builder.parse(file);

            ApiLibrary apiLibrary = null;
            NodeList libraryNodes = doc.getChildNodes();
            if (libraryNodes.getLength() > 0)
            {
                Node libraryNode = libraryNodes.item(0);
                String name = getAttributeValue(libraryNode, "name");
                String[] excludePackages = getAttributeValue(libraryNode, "excludePackages").replaceAll(" ", "")
                    .split(",");

                apiLibrary = new ApiLibrary(name);
                apiLibrary.setExcludePackages(Arrays.asList(excludePackages));

                Node childNode = libraryNode.getFirstChild();
                while(childNode != null)
                {
                    if (childNode.getNodeType() == Node.ELEMENT_NODE)
                    {
                        addNodePackage(apiLibrary, childNode);
                    }
                    childNode = childNode.getNextSibling();
                }
            }

            return apiLibrary;
        }
        catch(Exception e)
        {
            throw new RuntimeException("Unable to load API library data from the file " + file.getPath(), e);
        }
    }

    // *****************************************************************************************************
    // Support methods
    // *****************************************************************************************************

    private void addNodePackage(ApiLibrary apiLibrary, Node packageNode)
    {
        String name = getAttributeValue(packageNode, "name");
        ApiPackage apiPackage = apiLibrary.addPackage(name);

        Node childNode = packageNode.getFirstChild();
        while(childNode != null)
        {
            if (childNode.getNodeType() == Node.ELEMENT_NODE)
            {
                addNodeClass(apiPackage, childNode);
            }
            childNode = childNode.getNextSibling();
        }
    }

    private void addNodeClass(ApiPackage apiPackage, Node classNode)
    {
        String name = getAttributeValue(classNode, "name");
        ApiClass apiClass = apiPackage.addClass(name);

        Node childNode = classNode.getFirstChild();
        while(childNode != null)
        {
            if (childNode.getNodeType() == Node.ELEMENT_NODE)
            {
                addNodeMethod(apiPackage, apiClass, childNode);
            }
            childNode = childNode.getNextSibling();
        }
    }

    private void addNodeMethod(ApiPackage apiPackage, ApiClass apiClass, Node methodNode)
    {
        String name = getAttributeValue(methodNode, "simpleName");
        String modifiers = getAttributeValue(methodNode, "modifiers");
        String returnType = getAttributeValue(methodNode, "returnType");
        String parameters = getAttributeValue(methodNode, "parameters");
        apiClass.addMethod(name, modifiers, returnType, parameters);
    }

    private String getAttributeValue(Node elemNode, String attrName)
    {
        NamedNodeMap attributes = elemNode.getAttributes();
        if (attributes == null)
            throw new IllegalStateException("Element: " + elemNode.getNodeName() + " has no required attribute: "
                + attrName);

        Node node = attributes.getNamedItem(attrName);
        if (node == null)
            throw new IllegalStateException("Element: " + elemNode.getNodeName() + " has no required attribute: "
                + attrName);

        return node.getNodeValue();
    }

    private void addElementLibrary(Document doc, ApiLibrary apiLibrary)
    {
        Element libraryElement = doc.createElement("library");
        libraryElement.setAttribute("name", apiLibrary.getName());
        String excludePackages = apiLibrary.getPackagesToExclude().toString();
        excludePackages = excludePackages.replaceAll("\\[", "").replaceAll("\\]", "");
        libraryElement.setAttribute("excludePackages", excludePackages);

        doc.appendChild(libraryElement);

        for(ApiPackage apiPackage : apiLibrary.getPackages())
        {
            addElementPackage(doc, libraryElement, apiPackage);
        }
    }

    private void addElementPackage(Document doc, Element libraryElement, ApiPackage apiPackage)
    {
        Element packageElement = doc.createElement("package");
        packageElement.setAttribute("name", apiPackage.getName());
        libraryElement.appendChild(packageElement);

        for(ApiClass apiClass : apiPackage.getClasses())
        {
            addElementClass(doc, packageElement, apiClass);
        }
    }

    private void addElementClass(Document doc, Element packageElement, ApiClass apiClass)
    {
        Element classElement = doc.createElement("class");
        classElement.setAttribute("name", apiClass.getSimpleName());
        packageElement.appendChild(classElement);

        for(ApiMethod apiMethod : apiClass.getMethods())
        {
            Element methodElement = doc.createElement("method");
            methodElement.setAttribute("modifiers", apiMethod.getModifiers());
            methodElement.setAttribute("returnType", apiMethod.getReturnType());
            methodElement.setAttribute("simpleName", apiMethod.getSimpleName());
            methodElement.setAttribute("parameters", apiMethod.getParameters());
            classElement.appendChild(methodElement);
        }
    }

    private void writeDocument(Document doc, Writer writer) throws TransformerException
    {
        DOMSource domSource = new DOMSource(doc);
        TransformerFactory tf = TransformerFactory.newInstance();
        Transformer transformer = tf.newTransformer();
        transformer.setOutputProperty(OutputKeys.METHOD, "xml");
        transformer.setOutputProperty(OutputKeys.ENCODING, "ISO-8859-1");
        transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");
        transformer.setOutputProperty(OutputKeys.INDENT, "yes");

        StreamResult sr = new StreamResult(writer);
        transformer.transform(domSource, sr);
    }

}
